/**
 * Užduotis:
 * Įgyvendinti 1R klasifikavimo algoritmą.
 * Prototipas turi sugebėti nuskaityti duomenis pvz., iš .arff arba
 * .csv rinkmenos, pagal kurią apskaičiuojamas klasifikatorius,
 * (pasirenkamas komandinėje eilutėje atributas kaip klasė ir pasirinkamas
 * atributas kaip kintamasis). Taip pat, įgyvendinta program turi
 * sugebėti panaudoti klasifikatorių .arff arba .csv rinkmenai
 * pagal pasirinktą kintamąjį.
 * 
 * Programoje naudojamos išorinės bibliotekos:
 * hsqldb.jar
 * opencsv-2.2.jar
 * 
 * @author justyna.zachazevska@mif.stud.vu.lt
 */

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import au.com.bytecode.opencsv.CSVReader;

public class OneRClassifier {
	private String dataBaseName = "classifier_db";
	private String tableName = "classifier_table";
	private String fileName;
	private Map<String, String> columnNames; // key - column name; value - db
	// column name
	private Map<String, String> dataTypes;
	private DB db;
	private String variableName;
	private String className;
	private int thereshold;

	public OneRClassifier(String fileName, String variableName,
			String className, int thereshold) {
		this.fileName = fileName;
		this.variableName = variableName;
		this.className = className;
		this.thereshold = thereshold;
	}

	public void classify() {
		recognizeColumnNamesAndDataTypes();
		if (variableName != null)
			checkVariableValidity();
		prepareDB();
		printRules(getOneRClassificationRules());
		shutdownDB();
	}

	/**
	 * Grąžina suklasifikuotas taisykles.
	 * 
	 * @return
	 */

	private ArrayList<RuleInstance> getOneRClassificationRules() {
		if (className != null) {
			return getRulesForKnownClassName(className);
		} else {
			Map<String, ArrayList<RuleInstance>> rules = new LinkedHashMap<String, ArrayList<RuleInstance>>();

			Iterator<String> cIterator = this.columnNames.keySet().iterator();
			String colName;
			while (cIterator.hasNext()) {
				colName = cIterator.next();
				if (!colName.equals(variableName)) {
					rules.put(colName, getRulesForKnownClassName(colName));
				}
			}

			return computeBestClassRule(rules);
		}
	}

	/**
	 * Grąžina geriausios klasės taisykles, kurios suklasifikuoja kintamąjį su
	 * mažiausiu klaidos įverčiu.
	 * 
	 * @param rules
	 *            - sukonstruotos klasės stulpelio taisyklės
	 * @return
	 */

	private ArrayList<RuleInstance> computeBestClassRule(
			Map<String, ArrayList<RuleInstance>> rules) {

		Iterator<String> cIterator = rules.keySet().iterator();

		Map<String, Double> columnRates = new HashMap<String, Double>();

		String colName;
		while (cIterator.hasNext()) {
			colName = cIterator.next();

			ArrayList<RuleInstance> columnRules = rules.get(colName);

			double rate = computeColumnRate(columnRules);

			// System.out.println(colName + " " + rate);

			columnRates.put(colName, rate);
		}

		this.className = getMaxMapKeyByDuobleValue(columnRates);

		return rules.get(className);
	}

	/**
	 * Apskaičiuoja klasės klaidų santykį.
	 * 
	 * @param columnRules
	 *            - sukonstruotos klasės stulpelio taisyklės
	 * @return
	 */

	private double computeColumnRate(ArrayList<RuleInstance> columnRules) {
		int varCountTotal = 0;
		int classCountTotal = 0;
		for (int i = 0; i < columnRules.size(); i++) {
			varCountTotal += columnRules.get(i).getVariableCount();
			classCountTotal += columnRules.get(i).getClassCount();
		}

		double result = ((double) varCountTotal / (double) classCountTotal);

		return result;
	}

	/**
	 * Sukonstruoja taisykles nurodytam klasės stulpeliui.
	 * 
	 * @param classAttrName
	 *            - klasės stulpelio pavadinimas
	 * @return
	 */

	private ArrayList<RuleInstance> getRulesForKnownClassName(
			String classAttrName) {
		ArrayList<RuleInstance> rules = new ArrayList<RuleInstance>();

		// create nominal version for numeric values
		if ("numeric".equals(dataTypes.get(classAttrName))) {
			rules = getNumericAttributeRules(classAttrName, variableName);
		} else {
			Map<String, Integer> uniqueClassValues = getUniqueClassValues(classAttrName);
			Iterator<String> cIterator = uniqueClassValues.keySet().iterator();

			String classAttrValue = null, maxVariableNameByClass = null;
			Map<String, Integer> uniqueVariablesByClass = null;

			while (cIterator.hasNext()) {
				classAttrValue = (String) cIterator.next();

				uniqueVariablesByClass = getAttrVariablesByClass(classAttrName,
						classAttrValue, variableName);
				maxVariableNameByClass = getMaxMapKeyByIntegerValue(uniqueVariablesByClass);

				if (maxVariableNameByClass != null) {
					rules.add(new RuleInstance(classAttrValue,
							maxVariableNameByClass, uniqueClassValues.get(
									classAttrValue).intValue(),
							uniqueVariablesByClass.get(maxVariableNameByClass)
									.intValue()));
				}
			}

		}

		return rules;
	}

	/**
	 * Grąžina unikalias klasės stulpelio reikšmes.
	 * 
	 * @param classAttrName
	 *            - klasės stulpelio pavadinimas
	 * @return
	 */

	private Map<String, Integer> getUniqueClassValues(String classAttrName) {
		String classNameDB = columnNames.get(classAttrName);

		ArrayList<String> values = null;
		String query = "select " + classNameDB + ", count(" + classNameDB
				+ ") from " + tableName + " group by " + classNameDB + ";";
		try {
			values = db.query(query);
		} catch (SQLException e) {
			e.printStackTrace();
		}

		Map<String, Integer> result = new HashMap<String, Integer>(values
				.size() / 2);

		try {
			for (int i = 0; i < values.size() / 2; i++) {
				result.put(values.get(i * 2), Integer.parseInt(values
						.get(i * 2 + 1)));
			}
		} catch (NumberFormatException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Grąžina visas unikalias nurodyto kintamojo reikšmes, kurie eilutėje yra
	 * poroje su nurodyta klasės reikšme.
	 * 
	 * @param classAttrName
	 *            - klasės stulpelio pavadinimas
	 * @param classValueName
	 *            - klasės reikšmė
	 * @param variableAttrName
	 *            - kintamojo stulpelio pavadinimas
	 * @return
	 */

	public Map<String, Integer> getAttrVariablesByClass(String classAttrName,
			String classValueName, String variableAttrName) {
		String variableAttrNameDB = columnNames.get(variableName);
		String classAttrNameDB = columnNames.get(classAttrName);

		ArrayList<String> values = null;
		String query = "select " + variableAttrNameDB + ", count("
				+ variableAttrNameDB + ") from " + tableName + " where "
				+ classAttrNameDB + " = \'" + classValueName + "\' group by "
				+ variableAttrNameDB + ";";
		try {
			values = db.query(query);
		} catch (SQLException e) {
			e.printStackTrace();
		}

		Map<String, Integer> result = new HashMap<String, Integer>(values
				.size() / 2);

		try {
			for (int i = 0; i < values.size() / 2; i++) {
				result.put(values.get(i * 2), Integer.parseInt(values
						.get(i * 2 + 1)));
			}
		} catch (NumberFormatException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Grąžina sukonstruotas klasės stulpelio taisykles, kurios turi skaitines
	 * reikšmes.
	 * 
	 * @param classAttrName
	 *            - klasės stulpelio pavadinimas
	 * @param variableAttrName
	 *            - kintamojo stulpelio pavadinimas
	 * @return
	 */

	public ArrayList<RuleInstance> getNumericAttributeRules(
			String classAttrName, String variableAttrName) {
		ArrayList<RuleInstance> rules = new ArrayList<RuleInstance>();

		// -- Gets sorted numerical variable values with their class values
		ArrayList<String> values = getSortedVariableNumValues(classAttrName,
				variableAttrName);

		List<String> variableValues = new ArrayList<String>();
		List<Double> classValues = new ArrayList<Double>();

		try {
			for (int i = 0; i < values.size() / 2; i++) {
				variableValues.add(values.get(i * 2));
				classValues.add(Double.parseDouble(values.get(i * 2 + 1)));
			}
		} catch (NumberFormatException e) {
			e.printStackTrace();
		}
		// ----

		// Identifies indexes of breakpoints
		List<Integer> theresholdIndexes = getIntervalTheresholdIndexes(variableValues);

		// Calculates breakpoints
		double[] intervalBreakPoints = computeIntervalBreakPoints(classValues,
				theresholdIndexes);

		// Set unique nominal values
		int length = intervalBreakPoints.length;
		boolean hasLast = theresholdIndexes.get(theresholdIndexes.size() - 1) == classValues
				.size() - 1;
		String[] uniqueNominalValues = new String[(length == 1 && hasLast) ? length
				: length + 1];
		for (int i = 0; i < uniqueNominalValues.length; i++) {
			if (i == length) {
				uniqueNominalValues[i] = ">=" + intervalBreakPoints[i - 1];
			} else if (hasLast && (i == length - 1)) {
				uniqueNominalValues[i] = "<=" + intervalBreakPoints[i];
			} else {
				uniqueNominalValues[i] = "<" + intervalBreakPoints[i];

			}
		}

		// connect between nominal value and it's last index
		Map<String, Integer> nominalValIndexMapping = new LinkedHashMap<String, Integer>();
		for (int i = 0; i < uniqueNominalValues.length; i++) {
			int intervalIndex = (i == uniqueNominalValues.length - 1 ? classValues
					.size() - 1
					: theresholdIndexes.get(i));
			nominalValIndexMapping.put(uniqueNominalValues[i], intervalIndex);
		}

		// create rules
		Map<String, Integer> uniqueVariablesByClass = new HashMap<String, Integer>();

		int start, end, count, classCount = 0;
		for (int i = 0; i < nominalValIndexMapping.size(); i++) {
			start = (i == 0 ? 0 : (Integer) nominalValIndexMapping.values()
					.toArray()[i - 1] + 1);
			end = (Integer) nominalValIndexMapping.values().toArray()[i];

			for (int j = start; j <= end; j++) {
				if (uniqueVariablesByClass.containsKey(variableValues.get(j))) {
					count = uniqueVariablesByClass.get(variableValues.get(j));
					uniqueVariablesByClass.put(variableValues.get(j), ++count);
				} else {
					uniqueVariablesByClass.put(variableValues.get(j), 1);
				}
				classCount++;
			}

			String maxVariableByClass = getMaxMapKeyByIntegerValue(uniqueVariablesByClass);
			rules.add(new RuleInstance((String) nominalValIndexMapping.keySet()
					.toArray()[i], maxVariableByClass, classCount,
					uniqueVariablesByClass.get(maxVariableByClass)));
			classCount = 0;
			uniqueVariablesByClass.clear();

		}
		return rules;
	}

	/**
	 * Grąžina išrūšiuotas nurodyto kintamojo stulpelio skaitines reikšmes.
	 * 
	 * @param classAttrName
	 *            - klasės stulpelis
	 * @param variableAttrName
	 *            - kintamojo stulpelis
	 * @return
	 */

	private ArrayList<String> getSortedVariableNumValues(String classAttrName,
			String variableAttrName) {
		ArrayList<String> values = null;

		String variableAttrNameDB = columnNames.get(variableAttrName);
		String classAttrNameDB = columnNames.get(classAttrName);

		String query = "select " + variableAttrNameDB + ", " + classAttrNameDB
				+ " from " + tableName + " order by abs(" + classAttrNameDB
				+ ") asc;";
		try {
			values = db.query(query);
		} catch (SQLException e) {
			e.printStackTrace();
		}

		return values;
	}

	/**
	 * Paskaičiuoja skaitiniu reikšmių intervalų ribines reikšmes.
	 * 
	 * @param variableValues
	 *            - kintamojo stulpelio reikšmės
	 * @param theresholdIndexes
	 *            - intervalo indeksai
	 * @return
	 */

	private double[] computeIntervalBreakPoints(List<Double> variableValues,
			List<Integer> theresholdIndexes) {
		double[] breakPoints = new double[theresholdIndexes.size()];
		boolean hasLast = theresholdIndexes.get(theresholdIndexes.size() - 1) == variableValues
				.size() - 1;

		for (int i = 0; i < theresholdIndexes.size(); i++) {
			if (!hasLast) {
				double v = variableValues.get(theresholdIndexes.get(i)
						.intValue());
				double v1 = variableValues.get(theresholdIndexes.get(i)
						.intValue() + 1);

				breakPoints[i] = (v + v1) / 2;
			} else {
				breakPoints[i] = variableValues.get(theresholdIndexes.get(i)
						.intValue());
			}
			// System.out.print(breakPoints[i] + " ");
		}

		return breakPoints;
	}

	/**
	 * Grąžina skaitinių reikšmių ribinius intervalo indeksus.
	 * 
	 * @param classValues
	 *            - klasės stulpelio reikšmės
	 * @return
	 */

	private List<Integer> getIntervalTheresholdIndexes(List<String> classValues) {
		List<Integer> indexes = new ArrayList<Integer>();
		Map<String, Integer> temp = new HashMap<String, Integer>();
		int repeatsCount = 0;
		for (int i = 0; i < classValues.size(); i++) {
			if (temp.containsKey(classValues.get(i))) {

				// not last interval value
				repeatsCount = temp.get(classValues.get(i)).intValue();
				temp.put(classValues.get(i), ++repeatsCount);

				if (repeatsCount >= thereshold) {
					if (i != classValues.size() - 1
							&& classValues.get(i)
									.equals(classValues.get(i + 1))) {
						continue;
					} else {
						indexes.add(i);
						temp.clear();
					}
				}

			} else {
				temp.put(classValues.get(i), 1);
			}
		}

		return indexes;
	}

	/**
	 * Grąžina rakto pavadinimą, kurio sveikoji reikšmė map'e yra pati
	 * didžiausia.
	 * 
	 * @param map
	 * @return
	 */

	private String getMaxMapKeyByIntegerValue(Map<String, Integer> map) {
		String result = null;

		Iterator<String> mIterator = map.keySet().iterator();

		String key = null;
		Integer classCount = 0, max = 0;
		while (mIterator.hasNext()) {

			key = (String) mIterator.next();
			classCount = map.get(key);

			if (classCount > max) {
				max = classCount;
				result = key;
			}
		}

		return result;
	}

	/**
	 * Grąžina rakto reikšmę, kurio slankaus kablelio reikšmė map'e yra pati
	 * didžiausia.
	 * 
	 * @param map
	 * @return
	 */

	private String getMaxMapKeyByDuobleValue(Map<String, Double> map) {
		String result = null;

		Iterator<String> mIterator = map.keySet().iterator();

		String key = null;
		Double classCount = 0.0, max = 0.0;
		while (mIterator.hasNext()) {

			key = (String) mIterator.next();
			classCount = map.get(key);

			if (classCount > max) {
				max = classCount;
				result = key;
			}
		}

		return result;
	}

	/**
	 * Susieja duomenų bazę su cvs rinkmena.
	 * 
	 */

	private void prepareDB() {
		try {
			db = new DB(dataBaseName);
		} catch (Exception ex1) {
			// ex1.printStackTrace();
			return;
		}

		StringBuilder createTableStatement = new StringBuilder();
		createTableStatement.append("CREATE TEXT TABLE ").append(tableName)
				.append(" (");
		Object[] dbColNames = columnNames.values().toArray();
		Object[] columnDataTypes = dataTypes.values().toArray();

		for (int i = 0; i < dbColNames.length; i++) {
			createTableStatement.append(dbColNames[i]).append(" ").append(
					"numeric".equals(columnDataTypes[i]) ? "NUMERIC"
							: "VARCHAR(100)");

			if ((i + 1) < dbColNames.length) {
				createTableStatement.append(", ");
			}
		}
		createTableStatement.append(")");

		try {
			db.update(createTableStatement.toString());
			String a = "SET TABLE " + tableName + " SOURCE \"" + fileName
					+ ";all_quoted=true;ignore_first=true\"";
			db.update(a);
		} catch (SQLException ex2) {
			ex2.printStackTrace();
		}
	}

	/**
	 * Sustabdo duomenų bazę. Išvalo lentelę.
	 * 
	 */

	public void shutdownDB() {
		try {
			db.update("DROP TABLE " + tableName);
			db.shutdown();
		} catch (SQLException ex3) {
			return;
		}
	}

	/**
	 * Nuskaito rinkmenos stulpelių pavadinimus, nustato stulpalių duomenų
	 * tipus.
	 * 
	 * @return
	 */

	private void recognizeColumnNamesAndDataTypes() {
		columnNames = new LinkedHashMap<String, String>();
		dataTypes = new LinkedHashMap<String, String>();

		CSVReader reader;
		String[] columnNamesLine = null;
		String[] dataLine = null;

		try {
			reader = new CSVReader(new FileReader(fileName));
			columnNamesLine = reader.readNext();
			dataLine = reader.readNext();

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		for (int i = 0; i < columnNamesLine.length; i++) {
			// Set column names with db column names
			columnNames.put(columnNamesLine[i], "C" + i);

			// Set column names with datatypes
			try {
				Double.parseDouble(dataLine[i]);
				dataTypes.put(columnNamesLine[i], "numeric");
			} catch (Exception e) {
				dataTypes.put(columnNamesLine[i], "string");
			}
		}
	}

	/**
	 * Patikrina, ar programai nurodyto kintamojo stulpelio reiškšmės nėra
	 * skaitinio tipo.
	 * 
	 */

	private void checkVariableValidity() {
		if ("numeric".equals(dataTypes.get(variableName))) {
			System.out
					.println("Set variable attribute can't be a type of numeric!");
			System.exit(0);
		}
	}

	/**
	 * Atspausdina geriausios (nurodytos) klasės reikšmių taisykles konsolėje.
	 * 
	 * @param rules
	 */

	private void printRules(ArrayList<RuleInstance> rules) {
		System.out.println(this.className + " -> " + this.variableName
				+ " (errors/total)");
		System.out.println("------");
		for (int i = 0; i < rules.size(); i++) {
			System.out.println(rules.get(i).toString());
		}
		System.out.println("---");
	}

	/**
	 * Atpausdina vartotojui skirtą informaciją dėl programos paleidimo.
	 * 
	 */

	public static void usageMessage() {
		System.out
				.println("Usage: OneRClassifier.jar <fileName> <thereshold> <variableAttr> [classAttr]");
		System.out
				.println("Example: OneRClassifier.jar Test.csv 6 education education-num");
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		if (args.length < 1 && args.length > 4) {
			usageMessage();
		} else {
			OneRClassifier classifier = null;

			try {
				String fileName = args[0];
				int thereshold = Integer.parseInt(args[1]);
				String variableAttr = args[2];
				String classAttr = null;
				if (args.length > 3) {
					classAttr = args[3];
				}
				classifier = new OneRClassifier(fileName, variableAttr,
						classAttr, thereshold);
			} catch (Exception e) {
				usageMessage();
				System.exit(0);
			}

			long start = System.currentTimeMillis();
			classifier.classify();
			long end = System.currentTimeMillis();

			System.out.println("Completed: " + (double) (end - start) / 1000
					+ " s.");
		}
	}
}

/**
 * Klasė saugo atitinka vieną klasifikavimo taisyklę.
 * 
 * 
 */

class RuleInstance {
	/* Kintamojo pavadinimas */
	private String variableName;
	/* Klasės pavadinimas */
	private String className;
	/* Kintamojo teisingų reikšmių skaičius */
	private int variableCount;
	/* Visų klasės reikšmės egzempliorių skaičius */
	private int classCount;

	RuleInstance(String className, String variableName, int classCount,
			int variableCount) {
		this.variableName = variableName;
		this.className = className;
		this.variableCount = variableCount;
		this.classCount = classCount;
	}

	public String getVariableName() {
		return variableName;
	}

	public void setVariableName(String variableName) {
		this.variableName = variableName;
	}

	public String getClassName() {
		return className;
	}

	public void setClassName(String className) {
		this.className = className;
	}

	public int getVariableCount() {
		return variableCount;
	}

	public void setVariableCount(int variableCount) {
		this.variableCount = variableCount;
	}

	public int getClassCount() {
		return classCount;
	}

	public void setClassCount(int classCount) {
		this.classCount = classCount;
	}

	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append(className).append(" -> ").append(variableName).append(
				" (").append(classCount - variableCount).append("/").append(
				classCount).append(")");
		return builder.toString();
	}
}
